[Previous] [Next]

ActiveX Control Fundamentals

Visual Basic 5 and 6 give you all the tools you need to create powerful ActiveX controls, which you can then reuse in all your projects. More precisely, you can create two different types of ActiveX controls:

Visual Basic 5 was the first language that permitted programmers to create ActiveX controls using a visual approach. As you'll see in a moment, you can create powerful controls by simply grouping simpler controls together: These controls are known as constituent controls. By putting together a PictureBox and two scroll bar controls, for example, you can create an ActiveX control that can scroll its contents. Visual Basic also allows you to create an ActiveX control without using any constituent controls. These are the so-called owner-drawn ActiveX controls.

You should also keep this in mind when working with ActiveX controls: You're used to distinguishing between two distinct types of people interacting with your program—the developer and the user. To better understand how ActiveX controls behave, you need to take another role into account, the author of the control itself. The author's job is to prepare a control that will be used by the developer to deliver an application to the user. As you'll see, the author's and developer's perspectives are sometimes different, even though the two roles might be occupied by the same person. (That is, you might act as the author of the control and then as the developer who uses it.)

Creating the UserControl Module

In this section, I'll show you how to create a sort of super-TextBox control that adds extra capabilities to the regular TextBox control, such as filtering out invalid characters. The steps that you have to take any time you create a new Public ActiveX control are the following ones:

  1. Add a new ActiveX control project to the environment. This new project already includes a UserControl module. (Alternatively, manually add a UserControl module from the Project menu if you're creating a Private ActiveX control.)
  2. Give the project a meaningful name and a description. The former becomes the name of the control's library, and the latter is the string that appears in the Components dialog box for all the projects that use this control. In this example, we'll use the project's name SuperTB and the description An enhanced TextBox control.
  3. Click on the UserControl designer's window to give it the focus, and then enter a value for the Name property of the control in the Properties window. In this example, you can enter SuperTextBox.
  4. Place one or more constituent controls on the surface of the UserControl designer. In this example, you need to add a Label control and a TextBox control, as shown in Figure 17-1.

Click to view at full size.

Figure 17-1. The SuperTextBox control at design time.

You can use any intrinsic control as a constituent control of an ActiveX control except the OLE Container control (whose Toolbox icon is disabled when an ActiveX control designer has the focus). You can also use external ActiveX controls as constituent controls, but if you use one you should ascertain that you have the legal right to encapsulate it in your own control. All the ActiveX controls in the Visual Basic package except DBGrid can be freely reused in your own ActiveX control. Always carefully read the license agreements for third-party controls before encapsulating any in your own controls. You'll find more advice about these matters in the "Licensing" section near the end of this chapter. Finally, you can create ActiveX controls that don't use constituent controls, such as the SuperLabel control that you can find on the companion CD in the same directory as the SuperText project.

Now you can close the UserControl designer's window and switch to the Standard EXE project that you are using as a test client program. You'll notice that a new icon is now active in the Toolbox. Select it, and drop an instance of your brand new control on the form, as shown in Figure 17-2.

Congratulations! You've just created your first ActiveX control.

I want to draw your attention to one specific point in the previous description. You need to explicitly close the ActiveX control designer window before using the control on the test container form. If you omit this step, the icon in the Toolbox stays inactive. In fact, Visual Basic activates the ActiveX control and prepares it for siting only when you close the designer window. Siting refers to the instant an ActiveX control is placed on its container's surface.

You need to keep in mind that you have to deal with two different instances of the control, the design-time instance and the run-time instance. Unlike other Visual Basic modules, a UserControl module must be active even when the test project is in design mode. This is necessary because the control must react to the programmer's actions, such as entering the value of a property in the Properties window or resizing the control on the parent form. When you're working with the ActiveX control designer open, however, the control itself is in design mode and therefore can't be used in a form. To run an ActiveX control, you need to close its designer window, as I explained earlier.

Click to view at full size.

Figure 17-2. An instance of the SuperTextBox control on a testform. The Properties window includes a number of properties that have been defined for you by Visual Basic.

Running the ActiveX Control Interface Wizard

Our first version of the SuperTextBox control doesn't do anything useful yet, but you can run the client application and ensure that everything is working and that no error is raised. To turn this first prototype into a useful control, you need to add properties and methods and write the code that correctly implements the new features.

To complete the SuperTextBox control, you need to add all the properties that the user of this control expects to find, such as ForeColor, Text, and SelStart. A few of these properties must appear in the Properties window; others are run time_only properties. You also need to add other properties and methods that expand the basic TextBox functionality—for example the FormatMask property (which affects how the control's contents is formatted) or the Copy method (which copies the control's contents to the Clipboard).

In most cases, these properties and methods map directly to properties and methods of constituent controls: for example, the ForeColor and the Text properties map directly to the Text1 constituent control's properties with the same names, whereas the Caption property corresponds to the Caption property of the Label1 constituent control. This is similar to the concept of inheritance by delegation that you saw in Chapter 7.

To facilitate the task of creating the public interface of an ActiveX control and writing all the delegation code, Visual Basic includes the ActiveX Control Interface Wizard. This add-in is installed with the Visual Basic package, but you might need to explicitly load it from within the Add-In Manager dialog box.

In the first step of the wizard, you select the interface members, as shown in Figure 17-3. The wizard lists the properties, methods, and events that are exposed by the constituent controls and lets you select which ones should be made available to the outside. In this case, accept all those that are already in the rightmost list except BackStyle, and then add the following items: Alignment, Caption, Change, hWnd, Locked, MaxLength, MouseIcon, MousePointer, PasswordChar, SelLength, SelStart, SelText, Text, plus all the OLExxxx properties, methods, and events. These members ensure that the SuperTextBox control matches nearly all the capabilities of a regular TextBox control. A few properties have been left out—namely, MultiLine and ScrollBars. The reason for these exclusions will be clear later.

Click to view at full size.

Figure 17-3. The first step of the ActiveX Control Interface Wizard. You can also highlight multiple items and add all of them in one operation.

NOTE
Unfortunately, the ActiveX Control Interface Wizard lets you include many properties, methods, and events that you should never add to the public interface of your controls—for example, the ToolTipText, CausesValidation, WhatsThisHelpID, and Validate event. As a matter of fact, Visual Basic automatically adds these members to any ActiveX control that you create, so you don't need to specify them unless you plan to use the control in environments other than Visual Basic. More on this later.

In the next step, you define all the custom properties, methods, and events that your ActiveX control exposes. You should add the FormatMask, FormattedText, CaptionFont, CaptionForeColor, and CaptionBackColor properties; the Copy, Clear, Cut, and Paste methods; and the SelChange event.

In the third step, you define how the public members of the ActiveX control are mapped to the members of its constituent controls. For example, the Alignment public property should be mapped to the Text1 constituent control's Alignment property. The same holds true for the majority of the members in the list, and you can speed up mapping operations by selecting all of members and assigning them to the Text1 control, as shown in Figure 17-4.

Click to view at full size.

Figure 17-4. In the third step in the ActiveX Control Interface Wizard, you canmap multiple members by highlighting them in the leftmost list and then selectinga constituent control in the Control combo box on the right.

A few members—for example, the Caption property—map to the Label1 constituent control. You must specify the name of the original member in the constituent control when the two names differ, as in the case of the CaptionForeColor, CaptionBackColor, and CaptionFont properties that correspond to the Label1's ForeColor, BackColor, and Font properties, respectively. At other times, you have to map a public member to the UserControl itself—for example, the Refresh method.

There might be members that can't be directly mapped to any constituent control, and in the fourth step of the wizard you define how such members behave. For example, you declare that the Copy, Cut, Clear, and Paste methods are Subs by setting their return type to Empty. Similarly, you specify that FormatMask is a String property that can be read and modified either at design time or run time, whereas the FormattedText isn't available at design time and is read-only at run time. You should also specify an empty string as the default value for these three properties because even if you change the property type to String, the Wizard doesn't automatically change the value 0 that it initially set as the default. You must enter the argument list for all methods and events, as well as a brief description for each member, as shown in Figure 17-5.

The otherwise excellent ActiveX Control Interface Wizard has some limitations, though. For example, you can neither define properties with arguments, nor can you enter a description for all the custom properties—the CaptionFont and CaptionForeColor properties in this case—that are mapped to constituent controls.

CAUTION
Beware, international programmers! Being written in Visual Basic, the ActiveX Control Interface Wizard inherits a curious bug from the language if the Regional Setting established in the Control Panel isn't English. When Boolean constants True and False are concatenated in a string, the value you obtain is the localized string corresponding to that value. (For example, in Italian you get the strings "Vero" and "Falso", respectively.) Thus, in these circumstances the Wizard doesn't produce correct Visual Basic code, and you might have to edit it manually to run it. Or, if you prefer, you can set the Regional Setting to English if you plan to run the Wizard often in a programming session.

Click to view at full size.

Figure 17-5. In the fourth step in the ActiveX Control Interface Wizard, you decide the syntax of methods and events and whether properties are read/write or read-only at run time.

You're finally ready to click on the Finish button and generate all the code for the SuperTextBox control. If you go back to the instance of the control on the test form, you'll notice that the control has been grayed. This happens each time you change the public interface of the control. You can make the ActiveX control active again by right-clicking on its parent form and selecting the Update UserControls menu command.

Adding the Missing Pieces

Looking at the code that the ActiveX Control User Interface wizard generates is a good starting point for learning how ActiveX controls are implemented. Most of the time, you'll see that a UserControl module isn't different from a regular class module. One important note: The wizard adds several commented lines that it uses to keep track of how members are implemented. You should follow the warnings that come along with these lines and avoid deleting them or modifying them in any way.

Delegated properties, methods, and events

As I already explained, most of the code generated by the wizard does nothing but delegate the real action to the inner constituent controls. For example, see how the Text property is implemented:

Public Property Get Text() As String
    Text = Text1.Text
End Property
Public Property Let Text(ByVal New_Text As String)
    Text1.Text() = New_Text
    PropertyChanged "Text"
End Property

The PropertyChanged method informs the container environment—Visual Basic, in this case—that the property has been updated. This serves two purposes. First, at design time Visual Basic should know that the control has been updated and has to be saved in the FRM file. Second, at run time, if the Text property is bound to a database field, Visual Basic has to update the record. Data-aware ActiveX controls are described in the "Data Binding" section, later in this chapter.

The delegation mechanism also works for methods and events. For example, see how the SuperTextBox module traps the Text1 control's KeyPress event and exposes it to the outside, and notice how it delegates the Refresh method to the UserControl object:

' The declaration of the event
Event KeyPress(KeyAscii As Integer) 

Private Sub Text1_KeyPress(KeyAscii As Integer)
    RaiseEvent KeyPress(KeyAscii)
End Sub

Public Sub Refresh()
    UserControl.Refresh
End Sub

Custom properties

For all the public properties that aren't mapped to a property of a constituent control, the ActiveX Control Interface Wizard can't do anything but create a private member variable that stores the value assigned from the outside. For example, this is the code generated for the FormatMask custom property:

Dim m_FormatMask As String

Public Property Get FormatMask() As String
    FormatMask = m_FormatMask
End Property

Public Property Let FormatMask(ByVal New_FormatMask As String)
    m_FormatMask = New_FormatMask
    PropertyChanged "FormatMask"
End Property

Needless to say, you decide how such custom properties affect the behavior or the appearance of the SuperTextBox control. In this particular case, this property changes the behavior of another custom property, FormattedText, so you should modify the code generated by the wizard as follows:

Public Property Get FormattedText() As String
    FormattedText = Format$(Text, FormatMask)
End Property

The FormattedText property had been defined as read-only at run time, so the wizard has generated its Property Get procedure but not its Property Let procedure.

Custom methods

For each custom method you have added, the wizard generates the skeleton of a Sub or Function procedure. It's up to you to fill this template with code. For example, here's how you can implement the Copy and Clear methods:

Public Sub Copy()
    Clipboard.Clear
    Clipboard.SetText IIf(SelText <> "", SelText, Text)
End Sub

Public Sub Clear()
    If SelText <> "" Then SelText = "" Else Text = ""
End Sub

You might be tempted to use Text1.Text and Text1.SelText instead of Text and SelText in the previous code, but I advise you not to do it. If you use the public name of the property, your code is slightly slower, but you'll save a lot of time if you later decide to change the implementation of the Text property.

Custom events

You raise events from a UserControl module exactly as you would from within a regular class module. When you have a custom event that isn't mapped to any event of constituent controls, the wizard has generated only the event declaration for you because it can't understand when and where you want to raise it.

The SuperTextBox control exposes the SelChange event, which is raised when either the SelStart property or the SelLength property (or both) change. This event is useful when you want to display the current column on the status bar or when you want to enable or disable toolbar buttons depending on whether there's any selected text. To correctly implement this event, you must add two private variables and a private procedure that's called from multiple event procedures in the UserControl module:

Private saveSelStart As Long, saveSelLength As Long

' Raise the SelChange event if the cursor moved.
Private Sub CheckSelChange()
    If SelStart <> saveSelStart Or SelLength <> saveSelLength Then
        RaiseEvent SelChange
        saveSelStart = SelStart
        saveSelLength = SelLength
    End If
End Sub

Private Sub Text1_KeyUp(KeyCode As Integer, Shift As Integer)
    RaiseEvent KeyUp(KeyCode, Shift)
    CheckSelChange
End Sub

Private Sub Text1_Change()
    RaiseEvent Change
    CheckSelChange
End Sub

In the complete demonstration project that you can find on the companion CD, the CheckSelChange procedure is called from within Text1's MouseMove and MouseUp event procedures and also from within the Property Let SelStart and Property Let SelLength procedures.

Properties that map to multiple controls

Sometimes you might need to add custom code to correctly expose an event to the outside. Take, for example, the Click and DblClick events: You mapped them to the Text1 constituent control, but the UserControl module should raise an event also when the user clicks on the Label1 control. This means that you have to manually write the code that does the delegation:

Private Sub Label1_Click()
    RaiseEvent Click
End Sub

Private Sub Label1_DblClick()
    RaiseEvent DblClick
End Sub

You might also need to add delegation code when the same property applies to multiple constituent controls. Say that you want the ForeColor property to affect both the Text1 and Label1 controls. Since the wizard can map a property only to a single control, you must add some code (shown as boldface in the following listing) in the Property Let procedure that propagates the new value to the other constituent controls:

Public Property Let ForeColor(ByVal New_ForeColor As OLE_COLOR)
    Text1.ForeColor = New_ForeColor
    Label1.ForeColor = New_ForeColor
    PropertyChanged "ForeColor"
End Property

You don't need to modify the code in the corresponding Property Get procedure, however.

Persistent properties

The ActiveX Control Interface Wizard automatically generates the code that makes all the control's properties persistent via FRM files. The persistence mechanism is identical to the one used for persistable ActiveX components (which I explained in Chapter 16). In this case, however, you never have to explicitly ask an ActiveX control to save its own properties because the Visual Basic environment does it for you automatically if any of the control's properties have changed during the editing session in the environment

When the control is placed on a form, Visual Basic fires its UserControl_InitProperties event. In this event procedure, the control should initialize its properties to their default values. For example, this is the code that the wizard generates for the SuperTextBox control:

Const m_def_FormatMask = ""
Const m_def_FormattedText = ""

Private Sub UserControl_InitProperties()
    m_FormatMask = m_def_FormatMask
    m_FormattedText = m_def_FormattedText
End Sub

When Visual Basic saves the current form to an FRM file, it asks the ActiveX control to save itself by raising its UserControl_WriteProperties event:

Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
    Call PropBag.WriteProperty("FormatMask", m_FormatMask, m_def_FormatMask)
    Call PropBag.WriteProperty("FormattedText", m_FormattedText, _
        m_def_FormattedText)
    Call PropBag.WriteProperty("BackColor", Text1.BackColor, &H80000005)
    Call PropBag.WriteProperty("ForeColor", Text1.ForeColor, &H80000008)
    ' Other properties omitted....
End Sub

The third argument passed to the PropertyBag object's WriteProperty method is the default value for the property. When you're working with color properties, you usually pass hexadecimal constants that stand for system colors. For example, &H80000005 is the vbWindowBackground constant (the default background color), and &H80000008 is the vbWindowText constant (the default text color). Unfortunately, the wizard doesn't generate symbolic constants directly. For a complete list of supported system colors, use the Object Browser to enumerate the SystemColorConstants constants in the VBRUN library.

When Visual Basic reloads an FRM file, it fires the UserControl_ReadProperties event to let the ActiveX control restore its own properties:

Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
    m_FormatMask = PropBag.ReadProperty("FormatMask", m_def_FormatMask)
    m_FormattedText = PropBag.ReadProperty("FormattedText", _
        m_def_FormattedText)
    Text1.BackColor = PropBag.ReadProperty("BackColor", &H80000005)
    Text1.ForeColor = PropBag.ReadProperty("ForeColor", &H80000008)
    Set Text1.MouseIcon = PropBag.ReadProperty("MouseIcon", Nothing)
    ' Other properties omitted....
End Sub

Again, the last argument passed to the PropertyBag object's ReadProperty method is the default value of the property being retrieved. If you manually edit the code created by the wizard, be sure that you use the same constant in the InitProperties, WriteProperties, and ReadProperties event procedures.

The wizard does a good job of generating code for properties persistence, but in some cases you might need to fix it. For example, the preceding code directly assigns values to constituent controls' properties. While this approach is OK in most cases, it fails when the same property maps multiple controls, in which case you should assign the value to the Public property name. On the other hand, using the Public property name invokes its Property Let and Set procedures, which in turn call the PropertyChanged method and cause properties to be saved again even if they weren't modified during the current session. I'll show you how you can avoid this problem later in this chapter.

Moreover, the wizard creates more code than strictly necessary. For example, it generates the code that saves and restores properties that aren't available at design time (SelStart, SelText, SelLength, and FormattedText in this particular case). Dropping the corresponding statements from the ReadProperties and WriteProperties procedures makes your FRM files shorter and speeds up save and load operations.

The UserControl's Resize event

The UserControl object raises several events during the lifetime of an ActiveX control, and I'll describe all of them later in this chapter. One event, however, is especially important: the Resize event. This event fires at design time when the programmer drops the ActiveX control on the client form and also fires whenever the control itself is resized. As the author of the control, you must react to this event so that all the constituent controls move and resize accordingly. In this particular case, the position and size of constituent controls depend on whether the SuperTextBox control has a nonempty Caption:

Private Sub UserControl_Resize()
    On Error Resume Next
    If Caption <> "" Then
        Label1.Move 0, 0, ScaleWidth, Label1.Height
        Text1.Move 0, Label1.Height, ScaleWidth, _
            ScaleHeight - Label1.Height
    Else
        Text1.Move 0, 0, ScaleWidth, ScaleHeight
    End If
End Sub

The On Error statement serves to protect your application from errors that occur when the ActiveX control is shorter than the Label1 constituent control. The preceding code must execute also when the Caption property changes, so you need to add a statement to its Property Let procedure:

Public Property Let Caption(ByVal New_Caption As String)
    Label1.Caption = New_Caption
    PropertyChanged "Caption"
    Call UserControl_Resize
End Property